import axios from "axios";
import * as fs from "fs";
import * as gulp from "gulp";
import * as inquirer from "inquirer";
import * as jsonfile from "jsonfile";
import * as ora from "ora";
import * as path from "path";
import * as semver from "semver";
import * as util from "util";
import { CfgStorage } from "../../cfg";
import * as npm from "../../npm";
import { Validator as CrossRefValidator } from "../../pack";
import {
  ITask,
  resumeConsoleLog,
  suspendConsoleLog,
  task,
  TaskChain,
  tryResumeConsoleLog
} from "../../util";
import { assertConfigIsSet, downloadDllCfg, downloadTslintCfg } from "./create";
import { Manifest } from "./manifest";
import { dll2externals, pack } from "./pack";

const gulpTslint = require("gulp-tslint");
const prettier = require("gulp-prettier-plugin");
const map = require("map-stream");
const boxen = require("boxen");

const kinds = ["major", "minor", "patch"];
const readFile = util.promisify<string, string, string>(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
const readdir = util.promisify(fs.readdir);
const exists = util.promisify(fs.exists);

const kWMCAddressTest = "http://172.19.72.236:9001";
const kWMCAddressProd = "http://172.17.17.209:9001";
const kWMCServicePath = "/manager/bar/package/update";

const kWMCNotifyUrl = {
  test: kWMCAddressTest + kWMCServicePath,
  prod: kWMCAddressProd + kWMCServicePath
};

export const tellIfRefPkgIsXWidget = async (pkg: string) => {
  // just like a duck typing
  if (!pkg.startsWith("@qianmi")) return false;
  try {
    const data = await readFile(
      path.resolve(process.cwd(), "node_modules", pkg, "manifest.json"),
      "utf8"
    );
    return JSON.parse(data).isWidget === true;
  } catch (e) {
    return false;
  }
};

export class Publisher extends TaskChain {
  private pkg: string;
  private kind: string;
  private newVer: string;
  private localVer: string;
  private refPkgs: string[] = [];
  private refXWidgets: string[] = [];
  private dll: any;
  private envCode: "test" | "prod";

  constructor(kind: string) {
    super();
    this.kind = kind.toLowerCase();
    if (!kinds.includes(kind)) {
      throw new Error(`deformed kind ${kind}`);
    }
  }

  @task("检查配置项")
  async assertConfigIsSet() {
    return assertConfigIsSet(["gitlab.privateToken"]);
  }

  @task("读取包信息")
  async retrievePkgInfo() {
    const { name, version, dependencies = {} } = require(path.resolve(
      process.cwd(),
      "package.json"
    ));
    if (!name) {
      throw new Error("deformed package.json: missing field `name`");
    }
    if (!version) {
      throw new Error("deformed package.json: missing field `version`");
    }
    this.pkg = name;
    this.localVer = version;
    if (semver.valid(this.localVer) === null) {
      throw new Error(`version ${version} is not compliant with semver standard`);
    }
    this.refPkgs = Object.keys(dependencies);
  }

  // TODO: 打包完后在处理npm版本号
  @task("处理版本号")
  async resolveNewVer() {
    const isPkgExist = await npm.isPkgExist(this.pkg);
    // first publish
    if (!isPkgExist) {
      this.newVer = this.localVer;
      return;
    }
    const { view } = await npm.bootstrap();
    const data = await view([this.pkg, "version"], true);
    const curVer = Object.keys(data)[0]; // TODO 不传的话让用户选择
    const ver = semver.inc(curVer, this.kind as any);
    if (!ver) throw new Error("failed to produce new ver number");
    this.newVer = ver;
    const f = path.resolve(process.cwd(), "package.json");
    const json = require(f);
    json.version = this.newVer;
    await writeFile(f, JSON.stringify(json, null, 2));
  }

  @task("执行 prettier")
  async prettier() {
    const cfg = jsonfile.readFileSync("./.prettierrc", { throws: false });
    if (cfg === null) {
      throw new Error("missing or deformed .prettierrc");
    }
    return gulp
      .src(["./src/**/*.{ts,tsx,js,jsx}"])
      .pipe(prettier(cfg, { filter: true }) as any)
      .pipe(gulp.dest((file: any) => file.base));
  }

  @task("更新 tslint.json")
  async updateTslintCfg() {
    const projId = this.cfg.get<string>("xwidget.create.cfgProj");
    const api = this.cfg.get<string>("gitlab.api");
    const token = this.cfg.get<string>("gitlab.privateToken");
    const cfgPath = this.cfg.get<string>("xwidget.create.tslintCfgFilePath");
    await downloadTslintCfg(process.cwd(), projId, api, token, cfgPath);
  }

  @task("执行 tslint")
  async tslint() {
    const msg: string[] = [];
    return new Promise((resolve, reject) => {
      gulp
        .src(["./src/**/*.{ts,tsx}"])
        .pipe(
          gulpTslint({
            formatter: "stylish"
          })
        )
        .pipe(
          map((file: any, cb: any) => {
            const lint = file.tslint;
            if (lint.errorCount || lint.warningCount) {
              msg.push(lint.output);
            }
            // borrowed from gulp-tslint
            // see https://github.com/panuhorsmalahti/gulp-tslint/blob/master/index.js
            lint.failures = [];
            lint.fixes = [];
            cb(null, file);
          })
        )
        .on("error", reject)
        .on("end", resolve);
    }).then(() => {
      if (msg.length) {
        return Promise.reject(new Error(this.makeErrMsg(...msg)));
      }
      return Promise.resolve();
    });
  }

  // @task("检测交叉引用")
  // async crossReference() {
  //   const a = path.resolve(process.cwd(), "src/edit");
  //   const b = path.resolve(process.cwd(), "src/view");
  //   const errs = await new CrossRefValidator(a, b).run();
  //   if (errs.length) {
  //     throw new Error(errs.join("\n"));
  //   }
  // }

  @task("提取组件间依赖")
  async resolveXDependencies() {
    for (let i = 0, len = this.refPkgs.length; i < len; i++) {
      const pkg = this.refPkgs[i];
      const isXWidget = await tellIfRefPkgIsXWidget(pkg);
      if (isXWidget) this.refXWidgets.push(pkg);
    }
  }

  @task("下载 DLL 配置")
  async downloadDllCfg() {
    this.dll = await downloadDllCfg();
  }

  @task("选择环境")
  async chooseEnv() {
    // TODO env判断
    const ans: any = await inquirer.prompt({
      type: "list",
      name: "publish-env",
      message: "选择环境",
      choices: [
        {
          name: "Test",
          value: "test"
        },
        {
          name: "Prod",
          value: "prod"
        }
      ],
      default: "d2cStore"
    });
    this.envCode = ans["publish-env"];
  }

  @task("执行打包")
  async buildProd() {
    await pack(this.envCode, dll2externals(this.dll));
  }

  @task("整理资源信息")
  async collectOutInfo() {
    const info = {
      ...Manifest.getInstance().cfg,
      js: {
        edit: "edit.js",
        view: "view.js"
      },
      css: {
        edit: "edit.css",
        view: "view.css"
      },
      dependencies: []
    };
    const dist = path.resolve(process.cwd(), "dist");
    const files = await readdir(dist);
    if (!files.includes("edit.css")) info.css.edit = "";
    if (!files.includes("view.css")) info.css.view = "";
    await writeFile(path.resolve(dist, "manifest.json"), JSON.stringify(info, null, 2), {
      encoding: "utf8"
    });
  }

  @task("发布到 npm")
  async npmPublish() {
    const { publish: pub } = await npm.bootstrap();
    suspendConsoleLog();
    await pub(["."]);
    resumeConsoleLog();
  }

  @task("通知管理台")
  async notifyWMC() {
    const notify = async (env: "test" | "prod") => {
      try {
        const resp = await axios.request({
          method: "post",
          url: kWMCNotifyUrl[env],
          data: {
            name: this.pkg,
            version: this.newVer,
            type: Manifest.getInstance().get("type"),
            systemCode: Manifest.getInstance().get("systemCode")
          },
          timeout: 5000
        });
        if (!(resp.status >= 200 && resp.status <= 300))
          throw new Error("http code: " + resp.status);
        if (resp.data.status !== 1)
          throw new Error("server response:\n" + JSON.stringify(resp.data, null, 2));
      } catch (e) {
        let msg = "通知管理台失败: " + e.message;
        if (env === "prod") {
          msg += "\n\n发布生产环境需要开启 VPN，是否已经开启?".yellow;
        }
        throw new Error(msg);
      }
    };

    const envCode: any = this.envCode;
    await notify(envCode);
    if (envCode === "prod") {
      await notify("test"); // prod同时通知到 test 环境
    }
  }

  async enterTask(t: ITask): Promise<void> {
    t.extra.spinner = ora(t.name);
    if (["通知管理台", "选择环境"].indexOf(t.name) === -1) t.extra.spinner.start();
  }

  async leaveTask(t: ITask): Promise<void> {
    t.extra.spinner.succeed(t.name);
  }

  async leaveTaskError(t: ITask, e: Error): Promise<void> {
    tryResumeConsoleLog();

    t.extra.spinner.stop();
    console.log("\n" + "✘ ".red + t.name + ":\n");
    console.log(
      e.message
        .split("\n")
        .map(l => "  " + l)
        .join("\n")
    );
    if (e.stack) {
      console.log(`\n${"Call Stack:".red.bold}\n` + e.stack + "\n");
    }
    console.log(
      boxen(
        "It looks like a bug? Reporting it to http://git.dev.qianmi.com/OF2764/xwidget-cli/issues",
        { borderStyle: "double-single" }
      ).yellow
    );
  }

  async done(hasFailures: boolean): Promise<void> {
    if (hasFailures) return;
    console.log(`\n🎉  成功发布版本:\n`);
    console.log(`  ${this.pkg}@${this.newVer}`);
    console.log();
  }

  get cfg() {
    return CfgStorage.getInstance();
  }

  makeErrMsg(...lines: string[]) {
    return lines.join("\n");
  }
}

export const publish = async (kind: string) => {
  await new Publisher(kind).run();
};

export interface IManifest {
  name: "@qianmi/freelayout";
  wns: "x-site-ui/wechat/freelayoutbar";
  type: "bar" | "sub";
  js: {
    edit: "edit.js";
    view: "view.js";
  };
  css: {
    edit: "edit.css";
    view: "view.css";
  };
  dependencies: string[];
  defaultProps: {};
}
